Skip to content

Commit

Permalink
⚡ use qs_dart for query string encoding (#592)
Browse files Browse the repository at this point in the history
  • Loading branch information
techouse committed Apr 4, 2024
1 parent dd61ec1 commit 84471e4
Show file tree
Hide file tree
Showing 28 changed files with 2,920 additions and 213 deletions.
1 change: 1 addition & 0 deletions chopper/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ include: package:lints/recommended.yaml
analyzer:
exclude:
- "**.g.dart"
- "**.chopper.dart"
- "**.mocks.dart"
- "example/**"

Expand Down
1 change: 1 addition & 0 deletions chopper/lib/chopper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export 'src/constants.dart';
export 'src/extensions.dart';
export 'src/http_logging_interceptor.dart';
export 'src/interceptor.dart';
export 'src/list_format.dart';
export 'src/request.dart';
export 'src/response.dart';
export 'src/utils.dart' hide mapToQuery;
32 changes: 25 additions & 7 deletions chopper/lib/src/annotations.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';

import 'package:chopper/src/constants.dart';
import 'package:chopper/src/list_format.dart';
import 'package:chopper/src/request.dart';
import 'package:chopper/src/response.dart';
import 'package:meta/meta.dart';
Expand Down Expand Up @@ -190,14 +191,23 @@ sealed class Method {
/// Mark the body as optional to suppress warnings during code generation
final bool optionalBody;

/// Use brackets [ ] to when encoding
/// List format to use when encoding lists
///
/// - [ListFormat.repeat] `hxxp://path/to/script?foo=123&foo=456&foo=789` (default)
/// - [ListFormat.brackets] `hxxp://path/to/script?foo[]=123&foo[]=456&foo[]=789`
/// - [ListFormat.indices] `hxxp://path/to/script?foo[0]=123&foo[1]=456&foo[2]=789`
/// - [ListFormat.comma] `hxxp://path/to/script?foo=123,456,789`
final ListFormat? listFormat;

/// Use brackets `[ ]` to when encoding
///
/// - lists
/// hxxp://path/to/script?foo[]=123&foo[]=456&foo[]=789
/// `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;
/// `hxxp://path/to/script?user[name]=john&user[surname]=doe&user[age]=21`
@Deprecated('Use listFormat instead')
final bool? useBrackets;

/// Set to [true] to include query variables with null values. This includes nested maps.
/// The default is to exclude them.
Expand All @@ -223,16 +233,17 @@ sealed class Method {
/// ```
///
/// The above code produces hxxp://path/to/script&foo=foo_var&bar=&baz=baz_var
final bool includeNullQueryVars;
final bool? includeNullQueryVars;

/// {@macro Method}
const Method(
this.method, {
this.optionalBody = false,
this.path = '',
this.headers = const {},
this.useBrackets = false,
this.includeNullQueryVars = false,
this.listFormat,
@Deprecated('Use listFormat instead') this.useBrackets,
this.includeNullQueryVars,
});
}

Expand All @@ -247,6 +258,7 @@ final class Get extends Method {
super.optionalBody = true,
super.path,
super.headers,
super.listFormat,
super.useBrackets,
super.includeNullQueryVars,
}) : super(HttpMethod.Get);
Expand All @@ -265,6 +277,7 @@ final class Post extends Method {
super.optionalBody,
super.path,
super.headers,
super.listFormat,
super.useBrackets,
super.includeNullQueryVars,
}) : super(HttpMethod.Post);
Expand All @@ -281,6 +294,7 @@ final class Delete extends Method {
super.optionalBody = true,
super.path,
super.headers,
super.listFormat,
super.useBrackets,
super.includeNullQueryVars,
}) : super(HttpMethod.Delete);
Expand All @@ -299,6 +313,7 @@ final class Put extends Method {
super.optionalBody,
super.path,
super.headers,
super.listFormat,
super.useBrackets,
super.includeNullQueryVars,
}) : super(HttpMethod.Put);
Expand All @@ -316,6 +331,7 @@ final class Patch extends Method {
super.optionalBody,
super.path,
super.headers,
super.listFormat,
super.useBrackets,
super.includeNullQueryVars,
}) : super(HttpMethod.Patch);
Expand All @@ -332,6 +348,7 @@ final class Head extends Method {
super.optionalBody = true,
super.path,
super.headers,
super.listFormat,
super.useBrackets,
super.includeNullQueryVars,
}) : super(HttpMethod.Head);
Expand All @@ -348,6 +365,7 @@ final class Options extends Method {
super.optionalBody = true,
super.path,
super.headers,
super.listFormat,
super.useBrackets,
super.includeNullQueryVars,
}) : super(HttpMethod.Options);
Expand Down
29 changes: 29 additions & 0 deletions chopper/lib/src/list_format.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'package:qs_dart/qs_dart.dart' as qs show ListFormat;

/// An enum of all available list format options.
///
/// This is a wrapper around the [qs.ListFormat] enum.
enum ListFormat {
/// Use brackets to represent list items, for example
/// `foo[]=123&foo[]=456&foo[]=789`
brackets(qs.ListFormat.brackets),

/// Use commas to represent list items, for example
/// `foo=123,456,789`
comma(qs.ListFormat.comma),

/// Repeat the same key to represent list items, for example
/// `foo=123&foo=456&foo=789`
repeat(qs.ListFormat.repeat),

/// Use indices in brackets to represent list items, for example
/// `foo[0]=123&foo[1]=456&foo[2]=789`
indices(qs.ListFormat.indices);

const ListFormat(this.qsListFormat);

final qs.ListFormat qsListFormat;

@override
String toString() => name;
}
30 changes: 22 additions & 8 deletions chopper/lib/src/request.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:async' show Stream;

import 'package:chopper/src/extensions.dart';
import 'package:chopper/src/list_format.dart';
import 'package:chopper/src/utils.dart';
import 'package:equatable/equatable.dart' show EquatableMixin;
import 'package:http/http.dart' as http;
Expand All @@ -17,8 +18,10 @@ base class Request extends http.BaseRequest with EquatableMixin {
final Object? tag;
final bool multipart;
final List<PartValue> parts;
final bool useBrackets;
final bool includeNullQueryVars;
final ListFormat? listFormat;
@Deprecated('Use listFormat instead')
final bool? useBrackets;
final bool? includeNullQueryVars;

/// {@macro request}
Request(
Expand All @@ -31,8 +34,9 @@ base class Request extends http.BaseRequest with EquatableMixin {
this.multipart = false,
this.parts = const [],
this.tag,
this.useBrackets = false,
this.includeNullQueryVars = false,
this.listFormat,
@Deprecated('Use listFormat instead') this.useBrackets,
this.includeNullQueryVars,
}) : assert(
!baseUri.hasQuery,
'baseUri should not contain query parameters.'
Expand All @@ -45,6 +49,8 @@ base class Request extends http.BaseRequest with EquatableMixin {
baseUri,
uri,
{...uri.queryParametersAll, ...?parameters},
listFormat: listFormat,
// ignore: deprecated_member_use_from_same_package
useBrackets: useBrackets,
includeNullQueryVars: includeNullQueryVars,
),
Expand All @@ -62,7 +68,8 @@ base class Request extends http.BaseRequest with EquatableMixin {
Map<String, String>? headers,
bool? multipart,
List<PartValue>? parts,
bool? useBrackets,
ListFormat? listFormat,
@Deprecated('Use listFormat instead') bool? useBrackets,
bool? includeNullQueryVars,
Object? tag,
}) =>
Expand All @@ -75,6 +82,8 @@ base class Request extends http.BaseRequest with EquatableMixin {
headers: headers ?? this.headers,
multipart: multipart ?? this.multipart,
parts: parts ?? this.parts,
listFormat: listFormat ?? this.listFormat,
// ignore: deprecated_member_use_from_same_package
useBrackets: useBrackets ?? this.useBrackets,
includeNullQueryVars: includeNullQueryVars ?? this.includeNullQueryVars,
tag: tag ?? this.tag,
Expand All @@ -88,8 +97,9 @@ base class Request extends http.BaseRequest with EquatableMixin {
Uri baseUrl,
Uri url,
Map<String, dynamic> parameters, {
bool useBrackets = false,
bool includeNullQueryVars = false,
ListFormat? listFormat,
@Deprecated('Use listFormat instead') bool? useBrackets,
bool? includeNullQueryVars,
}) {
// If the request's url is already a fully qualified URL, we can use it
// as-is and ignore the baseUrl.
Expand All @@ -106,6 +116,8 @@ base class Request extends http.BaseRequest with EquatableMixin {

final String query = mapToQuery(
allParameters,
listFormat: listFormat,
// ignore: deprecated_member_use_from_same_package
useBrackets: useBrackets,
includeNullQueryVars: includeNullQueryVars,
);
Expand Down Expand Up @@ -239,6 +251,8 @@ base class Request extends http.BaseRequest with EquatableMixin {
headers,
multipart,
parts,
listFormat,
// ignore: deprecated_member_use_from_same_package
useBrackets,
includeNullQueryVars,
];
Expand Down
111 changes: 18 additions & 93 deletions chopper/lib/src/utils.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import 'dart:collection';

import 'package:chopper/chopper.dart';
import 'package:equatable/equatable.dart' show EquatableMixin;
import 'package:logging/logging.dart';
import 'package:qs_dart/qs_dart.dart' as qs;

/// Creates a new [Request] by copying [request] and adding a header with the
/// provided key [name] and value [value] to the result.
Expand Down Expand Up @@ -63,99 +63,24 @@ final chopperLogger = Logger('Chopper');
/// E.g., `{'foo': 'bar', 'ints': [ 1337, 42 ] }` will become 'foo=bar&ints=1337&ints=42'.
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,
ListFormat? listFormat,
@Deprecated('Use listFormat instead') bool? useBrackets,
bool? includeNullQueryVars,
}) {
final Set<_Pair<String, String>> pairs = {};

map.forEach((key, value) {
String name = Uri.encodeQueryComponent(key);

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, useBrackets: useBrackets));
} else if (value is Map<String, dynamic>) {
pairs.addAll(
_mapToQuery(
value,
prefix: name,
useBrackets: useBrackets,
includeNullQueryVars: includeNullQueryVars,
),
);
} else {
pairs.add(
_Pair<String, String>(name, _normalizeValue(value)),
);
}
} else {
if (includeNullQueryVars) {
pairs.add(_Pair<String, String>(name, ''));
}
}
});

return pairs;
}

Iterable<_Pair<String, String>> _iterableToQuery(
String name,
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 is DateTime
? value.toUtc().toIso8601String()
: value?.toString() ?? '',
);

final class _Pair<A, B> with EquatableMixin {
final A first;
final B second;
final bool useBrackets;

const _Pair(
this.first,
this.second, {
this.useBrackets = false,
});

@override
String toString() => useBrackets
? '$first${Uri.encodeQueryComponent('[]')}=$second'
: '$first=$second';

@override
List<Object?> get props => [
first,
second,
];
listFormat ??= useBrackets == true ? ListFormat.brackets : ListFormat.repeat;

return qs.encode(
map,
qs.EncodeOptions(
listFormat: listFormat.qsListFormat,
allowDots: listFormat == ListFormat.repeat,
encodeDotInKeys: listFormat == ListFormat.repeat,
encodeValuesOnly: listFormat == ListFormat.repeat,
skipNulls: includeNullQueryVars != true,
strictNullHandling: false,
serializeDate: (DateTime date) => date.toUtc().toIso8601String(),
),
);
}

bool isTypeOf<ThisType, OfType>() => _Instance<ThisType>() is _Instance<OfType>;
Expand Down
1 change: 1 addition & 0 deletions chopper/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies:
http: ^1.1.0
logging: ^1.2.0
meta: ^1.9.1
qs_dart: ^1.0.3

dev_dependencies:
build_runner: ^2.4.6
Expand Down
Loading

0 comments on commit 84471e4

Please sign in to comment.