diff --git a/chopper/lib/src/annotations.dart b/chopper/lib/src/annotations.dart index dafed63b..10784c92 100644 --- a/chopper/lib/src/annotations.dart +++ b/chopper/lib/src/annotations.dart @@ -161,11 +161,21 @@ class Method { /// Mark the body as optional to suppress warnings during code generation final bool optionalBody; + /// Use brackets [ ] to when encoding + /// + /// - lists + /// hxxp://path/to/script?foo[]=123&foo[]=456&foo[]=789 + /// + /// - maps + /// hxxp://path/to/script?user[name]=john&user[surname]=doe&user[age]=21 + final bool useBrackets; + const Method( this.method, { this.optionalBody = false, this.path = '', this.headers = const {}, + this.useBrackets = false, }); } @@ -176,6 +186,7 @@ class Get extends Method { super.optionalBody = true, super.path, super.headers, + super.useBrackets, }) : super(HttpMethod.Get); } @@ -188,6 +199,7 @@ class Post extends Method { super.optionalBody, super.path, super.headers, + super.useBrackets, }) : super(HttpMethod.Post); } @@ -198,6 +210,7 @@ class Delete extends Method { super.optionalBody = true, super.path, super.headers, + super.useBrackets, }) : super(HttpMethod.Delete); } @@ -210,6 +223,7 @@ class Put extends Method { super.optionalBody, super.path, super.headers, + super.useBrackets, }) : super(HttpMethod.Put); } @@ -221,6 +235,7 @@ class Patch extends Method { super.optionalBody, super.path, super.headers, + super.useBrackets, }) : super(HttpMethod.Patch); } @@ -231,6 +246,7 @@ class Head extends Method { super.optionalBody = true, super.path, super.headers, + super.useBrackets, }) : super(HttpMethod.Head); } @@ -240,6 +256,7 @@ class Options extends Method { super.optionalBody = true, super.path, super.headers, + super.useBrackets, }) : super(HttpMethod.Options); } diff --git a/chopper/lib/src/request.dart b/chopper/lib/src/request.dart index 8378e276..acd335ad 100644 --- a/chopper/lib/src/request.dart +++ b/chopper/lib/src/request.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; @@ -18,6 +17,7 @@ class Request { final Map parameters; final Map headers; final bool multipart; + final bool useBrackets; const Request( this.method, @@ -28,6 +28,7 @@ class Request { this.headers = const {}, this.multipart = false, this.parts = const [], + this.useBrackets = false, }); /// Makes a copy of this request, replacing original values with the given ones. @@ -37,23 +38,25 @@ class Request { dynamic body, Map? parameters, Map? headers, - Encoding? encoding, List? parts, bool? multipart, String? baseUrl, + bool? useBrackets, }) => Request( (method ?? this.method) as String, url ?? this.url, - baseUrl ?? this.baseUrl, body: body ?? this.body, parameters: parameters ?? this.parameters, headers: headers ?? this.headers, parts: parts ?? this.parts, multipart: multipart ?? this.multipart, + baseUrl ?? this.baseUrl, + useBrackets: useBrackets ?? this.useBrackets, ); - Uri _buildUri() => buildUri(baseUrl, url, parameters); + Uri _buildUri() => + buildUri(baseUrl, url, parameters, useBrackets: useBrackets); Map _buildHeaders() => {...headers}; @@ -110,7 +113,12 @@ class PartValueFile extends PartValue { /// Builds a valid URI from [baseUrl], [url] and [parameters]. /// /// If [url] starts with 'http://' or 'https://', baseUrl is ignored. -Uri buildUri(String baseUrl, String url, Map parameters) { +Uri buildUri( + String baseUrl, + String url, + Map parameters, { + bool useBrackets = false, +}) { // If the request's url is already a fully qualified URL, we can use it // as-is and ignore the baseUrl. Uri uri = url.startsWith('http://') || url.startsWith('https://') @@ -119,7 +127,7 @@ Uri buildUri(String baseUrl, String url, Map parameters) { ? Uri.parse('$baseUrl/$url') : Uri.parse('$baseUrl$url'); - String query = mapToQuery(parameters); + String query = mapToQuery(parameters, useBrackets: useBrackets); if (query.isNotEmpty) { if (uri.hasQuery) { query += '&${uri.query}'; diff --git a/chopper/lib/src/utils.dart b/chopper/lib/src/utils.dart index e4d17f7d..85ff8c62 100644 --- a/chopper/lib/src/utils.dart +++ b/chopper/lib/src/utils.dart @@ -56,29 +56,39 @@ 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 map) => _mapToQuery(map).join('&'); +String mapToQuery(Map map, {bool useBrackets = false}) => + _mapToQuery(map, useBrackets: useBrackets).join('&'); Iterable<_Pair> _mapToQuery( Map map, { String? prefix, + bool useBrackets = false, }) { final Set<_Pair> pairs = {}; map.forEach((key, value) { - if (value != null) { - String name = Uri.encodeQueryComponent(key); + String name = Uri.encodeQueryComponent(key); - if (prefix != null) { - name = '$prefix.$name'; - } + if (prefix != null) { + name = useBrackets + ? '$prefix${Uri.encodeQueryComponent('[')}$name${Uri.encodeQueryComponent(']')}' + : '$prefix.$name'; + } + if (value != null) { if (value is Iterable) { - pairs.addAll(_iterableToQuery(name, value)); + pairs.addAll(_iterableToQuery(name, value, useBrackets: useBrackets)); } else if (value is Map) { - pairs.addAll(_mapToQuery(value, prefix: name)); - } else if (value.toString().isNotEmpty) { - pairs.add(_Pair(name, _normalizeValue(value))); + pairs.addAll( + _mapToQuery(value, prefix: name, useBrackets: useBrackets), + ); + } else { + pairs.add( + _Pair(name, _normalizeValue(value)), + ); } + } else { + pairs.add(_Pair(name, '')); } }); @@ -87,20 +97,34 @@ Iterable<_Pair> _mapToQuery( Iterable<_Pair> _iterableToQuery( String name, - Iterable values, -) => - values.map((v) => _Pair(name, _normalizeValue(v))); + Iterable values, { + bool useBrackets = false, +}) => + values.where((value) => value?.toString().isNotEmpty ?? false).map( + (value) => _Pair( + name, + _normalizeValue(value), + useBrackets: useBrackets, + ), + ); -String _normalizeValue(value) => Uri.encodeComponent(value.toString()); +String _normalizeValue(value) => Uri.encodeComponent(value?.toString() ?? ''); class _Pair { final A first; final B second; + final bool useBrackets; - const _Pair(this.first, this.second); + const _Pair( + this.first, + this.second, { + this.useBrackets = false, + }); @override - String toString() => '$first=$second'; + String toString() => useBrackets + ? '$first${Uri.encodeQueryComponent('[]')}=$second' + : '$first=$second'; } bool isTypeOf() => _Instance() is _Instance; diff --git a/chopper/test/base_test.dart b/chopper/test/base_test.dart index 99d81231..cb281caf 100644 --- a/chopper/test/base_test.dart +++ b/chopper/test/base_test.dart @@ -107,7 +107,7 @@ void main() { final httpClient = MockClient((request) async { expect( request.url.toString(), - equals('$baseUrl/test/query'), + equals('$baseUrl/test/query?name=&int=&default_value='), ); expect(request.method, equals('GET')); @@ -129,7 +129,7 @@ void main() { final httpClient = MockClient((request) async { expect( request.url.toString(), - equals('$baseUrl/test/query?default_value=42'), + equals('$baseUrl/test/query?name=&int=&default_value=42'), ); expect(request.method, equals('GET')); @@ -888,4 +888,149 @@ void main() { httpClient.close(); }); + + test('List query param', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/list_query_param' + '?value=foo' + '&value=bar' + '&value=baz'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.getUsingListQueryParam([ + 'foo', + 'bar', + 'baz', + ]); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + + test('List query param with brackets', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/list_query_param_with_brackets' + '?value%5B%5D=foo' + '&value%5B%5D=bar' + '&value%5B%5D=baz'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.getUsingListQueryParamWithBrackets([ + 'foo', + 'bar', + 'baz', + ]); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + + test('Map query param using default dot QueryMapSeparator', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/map_query_param' + '?value.bar=baz' + '&value.zap=abc' + '&value.etc.abc=def' + '&value.etc.ghi=jkl' + '&value.etc.mno.opq=rst' + '&value.etc.mno.uvw=xyz' + '&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(); + + final response = await service.getUsingMapQueryParam({ + 'bar': 'baz', + 'zap': 'abc', + 'etc': { + 'abc': 'def', + 'ghi': 'jkl', + 'mno': { + 'opq': 'rst', + 'uvw': 'xyz', + 'list': ['a', 123, false], + }, + }, + }); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + + test('Map query param with brackets QueryMapSeparator', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/map_query_param_with_brackets' + '?value%5Bbar%5D=baz' + '&value%5Bzap%5D=abc' + '&value%5Betc%5D%5Babc%5D=def' + '&value%5Betc%5D%5Bghi%5D=jkl' + '&value%5Betc%5D%5Bmno%5D%5Bopq%5D=rst' + '&value%5Betc%5D%5Bmno%5D%5Buvw%5D=xyz' + '&value%5Betc%5D%5Bmno%5D%5Blist%5D%5B%5D=a' + '&value%5Betc%5D%5Bmno%5D%5Blist%5D%5B%5D=123' + '&value%5Betc%5D%5Bmno%5D%5Blist%5D%5B%5D=false'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = + await service.getUsingMapQueryParamWithBrackets({ + 'bar': 'baz', + 'zap': 'abc', + 'etc': { + 'abc': 'def', + 'ghi': 'jkl', + 'mno': { + 'opq': 'rst', + 'uvw': 'xyz', + 'list': ['a', 123, false], + }, + }, + }); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); } diff --git a/chopper/test/test_service.chopper.dart b/chopper/test/test_service.chopper.dart index 01d5757e..d0a43b5c 100644 --- a/chopper/test/test_service.chopper.dart +++ b/chopper/test/test_service.chopper.dart @@ -488,4 +488,60 @@ class _$HttpTestService extends HttpTestService { ); return client.send($request); } + + @override + Future> getUsingListQueryParam(List value) { + final String $url = '/test/list_query_param'; + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParamWithBrackets( + List value) { + final String $url = '/test/list_query_param_with_brackets'; + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParam(Map value) { + final String $url = '/test/map_query_param'; + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamWithBrackets( + Map value) { + final String $url = '/test/map_query_param_with_brackets'; + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } } diff --git a/chopper/test/test_service.dart b/chopper/test/test_service.dart index 03abc239..7789b361 100644 --- a/chopper/test/test_service.dart +++ b/chopper/test/test_service.dart @@ -138,6 +138,26 @@ abstract class HttpTestService extends ChopperService { @Post(path: 'no-body') Future noBody(); + + @Get(path: '/list_query_param') + Future> getUsingListQueryParam( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_brackets', useBrackets: true) + Future> getUsingListQueryParamWithBrackets( + @Query('value') List value, + ); + + @Get(path: '/map_query_param') + Future> getUsingMapQueryParam( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_brackets', useBrackets: true) + Future> getUsingMapQueryParamWithBrackets( + @Query('value') Map value, + ); } Request customConvertRequest(Request req) { diff --git a/chopper/test/utils_test.dart b/chopper/test/utils_test.dart new file mode 100644 index 00000000..649a4651 --- /dev/null +++ b/chopper/test/utils_test.dart @@ -0,0 +1,277 @@ +import 'package:chopper/src/utils.dart'; +import 'package:test/test.dart'; + +void main() { + group('mapToQuery single', () { + , String>{ + {'foo': null}: 'foo=', + {'foo': ''}: 'foo=', + {'foo': ' '}: 'foo=%20', + {'foo': ' '}: 'foo=%20%20', + {'foo': '\t'}: 'foo=%09', + {'foo': '\t\t'}: 'foo=%09%09', + {'foo': 'null'}: 'foo=null', + {'foo': 'bar'}: 'foo=bar', + {'foo': ' bar '}: 'foo=%20bar%20', + {'foo': '\tbar\t'}: 'foo=%09bar%09', + {'foo': '\t\tbar\t\t'}: 'foo=%09%09bar%09%09', + {'foo': 123}: 'foo=123', + {'foo': 0}: 'foo=0', + {'foo': -0.01}: 'foo=-0.01', + {'foo': '0.00'}: 'foo=0.00', + {'foo': 123.456}: 'foo=123.456', + {'foo': 123.450}: 'foo=123.45', + {'foo': -123.456}: 'foo=-123.456', + {'foo': true}: 'foo=true', + {'foo': false}: 'foo=false', + }.forEach((map, query) => + test('$map -> $query', () => expect(mapToQuery(map), query))); + }); + + group('mapToQuery multiple', () { + , String>{ + {'foo': null, 'baz': null}: 'foo=&baz=', + {'foo': '', 'baz': ''}: 'foo=&baz=', + {'foo': null, 'baz': ''}: 'foo=&baz=', + {'foo': '', 'baz': null}: 'foo=&baz=', + {'foo': 'bar', 'baz': ''}: 'foo=bar&baz=', + {'foo': null, 'baz': 'etc'}: 'foo=&baz=etc', + {'foo': '', 'baz': 'etc'}: 'foo=&baz=etc', + {'foo': 'bar', 'baz': 'etc'}: 'foo=bar&baz=etc', + {'foo': 'null', 'baz': 'null'}: 'foo=null&baz=null', + {'foo': ' ', 'baz': ' '}: 'foo=%20&baz=%20', + {'foo': '\t', 'baz': '\t'}: 'foo=%09&baz=%09', + {'foo': 123, 'baz': 456}: 'foo=123&baz=456', + {'foo': 0, 'baz': 0}: 'foo=0&baz=0', + {'foo': '0.00', 'baz': '0.00'}: 'foo=0.00&baz=0.00', + {'foo': 123.456, 'baz': 789.012}: 'foo=123.456&baz=789.012', + {'foo': 123.450, 'baz': 789.010}: 'foo=123.45&baz=789.01', + {'foo': -123.456, 'baz': -789.012}: 'foo=-123.456&baz=-789.012', + {'foo': true, 'baz': true}: 'foo=true&baz=true', + {'foo': false, 'baz': false}: 'foo=false&baz=false', + }.forEach((map, query) => + test('$map -> $query', () => expect(mapToQuery(map), query))); + }); + + group('mapToQuery lists', () { + , String>{ + { + 'foo': ['bar', 'baz', 'etc'], + }: 'foo=bar&foo=baz&foo=etc', + { + 'foo': ['bar', 123, 456.789, 0, -123, -456.789], + }: 'foo=bar&foo=123&foo=456.789&foo=0&foo=-123&foo=-456.789', + { + 'foo': ['', 'baz', 'etc'], + }: 'foo=baz&foo=etc', + { + 'foo': ['bar', '', 'etc'], + }: 'foo=bar&foo=etc', + { + 'foo': ['bar', 'baz', ''], + }: 'foo=bar&foo=baz', + { + 'foo': [null, 'baz', 'etc'], + }: 'foo=baz&foo=etc', + { + 'foo': ['bar', null, 'etc'], + }: 'foo=bar&foo=etc', + { + 'foo': ['bar', 'baz', null], + }: 'foo=bar&foo=baz', + { + 'foo': ['bar', 'baz', ' '], + }: 'foo=bar&foo=baz&foo=%20', + { + 'foo': ['bar', 'baz', '\t'], + }: 'foo=bar&foo=baz&foo=%09', + { + 'foo': ['bar', 'baz', 'etc'], + 'bar': 'baz', + 'etc': '', + 'xyz': null, + }: 'foo=bar&foo=baz&foo=etc&bar=baz&etc=&xyz=', + }.forEach((map, query) => + test('$map -> $query', () => expect(mapToQuery(map), query))); + }); + + group('mapToQuery lists with brackets', () { + , String>{ + { + 'foo': ['bar', 'baz', 'etc'], + }: 'foo%5B%5D=bar&foo%5B%5D=baz&foo%5B%5D=etc', + { + 'foo': ['bar', 123, 456.789, 0, -123, -456.789], + }: 'foo%5B%5D=bar&foo%5B%5D=123&foo%5B%5D=456.789&foo%5B%5D=0&foo%5B%5D=-123&foo%5B%5D=-456.789', + { + 'foo': ['', 'baz', 'etc'], + }: 'foo%5B%5D=baz&foo%5B%5D=etc', + { + 'foo': ['bar', '', 'etc'], + }: 'foo%5B%5D=bar&foo%5B%5D=etc', + { + 'foo': ['bar', 'baz', ''], + }: 'foo%5B%5D=bar&foo%5B%5D=baz', + { + 'foo': [null, 'baz', 'etc'], + }: 'foo%5B%5D=baz&foo%5B%5D=etc', + { + 'foo': ['bar', null, 'etc'], + }: 'foo%5B%5D=bar&foo%5B%5D=etc', + { + 'foo': ['bar', 'baz', null], + }: 'foo%5B%5D=bar&foo%5B%5D=baz', + { + 'foo': ['bar', 'baz', ' '], + }: 'foo%5B%5D=bar&foo%5B%5D=baz&foo%5B%5D=%20', + { + 'foo': ['bar', 'baz', '\t'], + }: 'foo%5B%5D=bar&foo%5B%5D=baz&foo%5B%5D=%09', + { + 'foo': ['bar', 'baz', 'etc'], + 'bar': 'baz', + 'etc': '', + 'xyz': null, + }: 'foo%5B%5D=bar&foo%5B%5D=baz&foo%5B%5D=etc&bar=baz&etc=&xyz=', + }.forEach( + (map, query) => test( + '$map -> $query', + () => expect( + mapToQuery(map, useBrackets: true), + query, + ), + ), + ); + }); + + group('mapToQuery maps', () { + , String>{ + { + 'foo': {'bar': 'baz'}, + }: 'foo.bar=baz', + { + 'foo': {'bar': ''}, + }: 'foo.bar=', + { + 'foo': {'bar': null}, + }: 'foo.bar=', + { + 'foo': {'bar': ' '}, + }: 'foo.bar=%20', + { + 'foo': {'bar': '\t'}, + }: 'foo.bar=%09', + { + 'foo': {'bar': 'baz', 'etc': 'xyz', 'space': ' ', 'tab': '\t'}, + }: 'foo.bar=baz&foo.etc=xyz&foo.space=%20&foo.tab=%09', + { + 'foo': { + 'bar': 'baz', + 'int': 123, + 'double': 456.789, + 'zero': 0, + 'negInt': -123, + 'negDouble': -456.789, + 'emptyString': '', + 'nullValue': null, + 'space': ' ', + 'tab': '\t', + 'list': ['a', 123, false], + }, + }: 'foo.bar=baz&foo.int=123&foo.double=456.789&foo.zero=0&foo.negInt=-123&foo.negDouble=-456.789&foo.emptyString=&foo.nullValue=&foo.space=%20&foo.tab=%09&foo.list=a&foo.list=123&foo.list=false', + { + 'foo': {'bar': 'baz'}, + 'etc': 'xyz', + }: 'foo.bar=baz&etc=xyz', + { + 'foo': { + 'bar': 'baz', + 'zap': 'abc', + 'etc': { + 'abc': 'def', + 'ghi': 'jkl', + 'mno': { + 'opq': 'rst', + 'uvw': 'xyz', + 'aab': [ + 'bbc', + 'ccd', + 'eef', + ], + }, + }, + }, + }: 'foo.bar=baz&foo.zap=abc&foo.etc.abc=def&foo.etc.ghi=jkl&foo.etc.mno.opq=rst&foo.etc.mno.uvw=xyz&foo.etc.mno.aab=bbc&foo.etc.mno.aab=ccd&foo.etc.mno.aab=eef', + }.forEach((map, query) => + test('$map -> $query', () => expect(mapToQuery(map), query))); + }); + + group('mapToQuery maps with brackets', () { + , String>{ + { + 'foo': {'bar': 'baz'}, + }: 'foo%5Bbar%5D=baz', + { + 'foo': {'bar': ''}, + }: 'foo%5Bbar%5D=', + { + 'foo': {'bar': null}, + }: 'foo%5Bbar%5D=', + { + 'foo': {'bar': ' '}, + }: 'foo%5Bbar%5D=%20', + { + 'foo': {'bar': '\t'}, + }: 'foo%5Bbar%5D=%09', + { + 'foo': {'bar': 'baz', 'etc': 'xyz', 'space': ' ', 'tab': '\t'}, + }: 'foo%5Bbar%5D=baz&foo%5Betc%5D=xyz&foo%5Bspace%5D=%20&foo%5Btab%5D=%09', + { + 'foo': { + 'bar': 'baz', + 'int': 123, + 'double': 456.789, + 'zero': 0, + 'negInt': -123, + 'negDouble': -456.789, + 'emptyString': '', + 'nullValue': null, + 'space': ' ', + 'tab': '\t', + 'list': ['a', 123, false], + }, + }: 'foo%5Bbar%5D=baz&foo%5Bint%5D=123&foo%5Bdouble%5D=456.789&foo%5Bzero%5D=0&foo%5BnegInt%5D=-123&foo%5BnegDouble%5D=-456.789&foo%5BemptyString%5D=&foo%5BnullValue%5D=&foo%5Bspace%5D=%20&foo%5Btab%5D=%09&foo%5Blist%5D%5B%5D=a&foo%5Blist%5D%5B%5D=123&foo%5Blist%5D%5B%5D=false', + { + 'foo': {'bar': 'baz'}, + 'etc': 'xyz', + }: 'foo%5Bbar%5D=baz&etc=xyz', + { + 'foo': { + 'bar': 'baz', + 'zap': 'abc', + 'etc': { + 'abc': 'def', + 'ghi': 'jkl', + 'mno': { + 'opq': 'rst', + 'uvw': 'xyz', + 'aab': [ + 'bbc', + 'ccd', + 'eef', + ], + }, + }, + }, + }: 'foo%5Bbar%5D=baz&foo%5Bzap%5D=abc&foo%5Betc%5D%5Babc%5D=def&foo%5Betc%5D%5Bghi%5D=jkl&foo%5Betc%5D%5Bmno%5D%5Bopq%5D=rst&foo%5Betc%5D%5Bmno%5D%5Buvw%5D=xyz&foo%5Betc%5D%5Bmno%5D%5Baab%5D%5B%5D=bbc&foo%5Betc%5D%5Bmno%5D%5Baab%5D%5B%5D=ccd&foo%5Betc%5D%5Bmno%5D%5Baab%5D%5B%5D=eef', + }.forEach( + (map, query) => test( + '$map -> $query', + () => expect( + mapToQuery(map, useBrackets: true), + query, + ), + ), + ); + }); +} diff --git a/chopper_generator/analysis_options.yaml b/chopper_generator/analysis_options.yaml index ead6c70c..3a82dc3b 100644 --- a/chopper_generator/analysis_options.yaml +++ b/chopper_generator/analysis_options.yaml @@ -14,7 +14,7 @@ dart_code_metrics: cyclomatic-complexity: 20 number-of-arguments: 4 maximum-nesting-level: 5 - number-of-parameters: 5 + number-of-parameters: 6 source-lines-of-code: 250 metrics-exclude: - test/** diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 4badfc70..6dda3c4a 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -309,6 +309,8 @@ class ChopperGenerator extends GeneratorForAnnotation { ); } + final bool useBrackets = getUseBrackets(method); + blocks.add( declareFinal(_requestVar, type: refer('Request')) .assign( @@ -318,6 +320,7 @@ class ChopperGenerator extends GeneratorForAnnotation { useQueries: hasQuery, useHeaders: headers != null, hasParts: hasParts, + useBrackets: useBrackets, ), ) .statement, @@ -490,6 +493,7 @@ class ChopperGenerator extends GeneratorForAnnotation { bool hasParts = false, bool useQueries = false, bool useHeaders = false, + bool useBrackets = false, }) { final List params = [ literal(getMethodName(method)), @@ -516,6 +520,10 @@ class ChopperGenerator extends GeneratorForAnnotation { namedParams['headers'] = refer(_headersVar); } + if (useBrackets) { + namedParams['useBrackets'] = literalBool(useBrackets); + } + return refer('Request').newInstance(params, namedParams); } @@ -542,7 +550,9 @@ class ChopperGenerator extends GeneratorForAnnotation { ]; list.add(refer( - 'PartValue<${p.type.getDisplayString(withNullability: p.type.isNullable)}>', + 'PartValue<${p.type.getDisplayString( + withNullability: p.type.isNullable, + )}>', ).newInstance(params)); }); fileFields.forEach((p, ConstantReader r) { @@ -618,6 +628,9 @@ String getMethodPath(ConstantReader method) => method.read('path').stringValue; String getMethodName(ConstantReader method) => method.read('method').stringValue; +bool getUseBrackets(ConstantReader method) => + method.peek('useBrackets')?.boolValue ?? false; + extension DartTypeExtension on DartType { bool get isNullable => nullabilitySuffix != NullabilitySuffix.none; }