Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⚡ use qs_dart for query string encoding #592

Merged
merged 12 commits into from
Apr 4, 2024
Merged
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;

Check warning on line 28 in chopper/lib/src/list_format.dart

View check run for this annotation

Codecov / codecov/patch

chopper/lib/src/list_format.dart#L27-L28

Added lines #L27 - L28 were not covered by tests
}
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.1+1

dev_dependencies:
build_runner: ^2.4.6
Expand Down
Loading
Loading