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

✨ decode: add throwOnLimitExceeded option #26

Merged
merged 5 commits into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 61 additions & 9 deletions lib/src/extensions/decode.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,25 @@
),
);

static dynamic _parseArrayValue(dynamic val, DecodeOptions options) =>
val is String && val.isNotEmpty && options.comma && val.contains(',')
? val.split(',')
: val;
static dynamic _parseListValue(
dynamic val,
DecodeOptions options,
int currentListLength,
) {
if (val is String && val.isNotEmpty && options.comma && val.contains(',')) {
return val.split(',');
}
techouse marked this conversation as resolved.
Show resolved Hide resolved

if (options.throwOnLimitExceeded &&
currentListLength >= options.listLimit) {
throw RangeError(
'List limit exceeded. '
'Only ${options.listLimit} element${options.listLimit == 1 ? '' : 's'} allowed in a list.',
);
}

return val;
}

static Map<String, dynamic> _parseQueryStringValues(
String str, [
Expand All @@ -23,12 +38,23 @@
(options.ignoreQueryPrefix ? str.replaceFirst('?', '') : str)
.replaceAll(RegExp(r'%5B', caseSensitive: false), '[')
.replaceAll(RegExp(r'%5D', caseSensitive: false), ']');
final num? limit = options.parameterLimit == double.infinity

final int? limit = options.parameterLimit == double.infinity
? null
: options.parameterLimit;
: options.parameterLimit.toInt();

techouse marked this conversation as resolved.
Show resolved Hide resolved
final Iterable<String> parts = limit != null && limit > 0
? cleanStr.split(options.delimiter).take(limit.toInt())
? cleanStr
.split(options.delimiter)
.take(options.throwOnLimitExceeded ? limit + 1 : limit)
: cleanStr.split(options.delimiter);

if (options.throwOnLimitExceeded && limit != null && parts.length > limit) {
throw RangeError(
'Parameter limit exceeded. Only $limit parameter${limit == 1 ? '' : 's'} allowed.',
);
}

int skipIndex = -1; // Keep track of where the utf8 sentinel was found
int i;

Expand Down Expand Up @@ -65,7 +91,13 @@
} else {
key = options.decoder(part.slice(0, pos), charset: charset);
val = Utils.apply<dynamic>(
_parseArrayValue(part.slice(pos + 1), options),
_parseListValue(
part.slice(pos + 1),
options,
obj.containsKey(key) && obj[key] is List
? (obj[key] as List).length
: 0,
),
(dynamic val) => options.decoder(val, charset: charset),
);
}
Expand Down Expand Up @@ -102,7 +134,27 @@
DecodeOptions options,
bool valuesParsed,
) {
dynamic leaf = valuesParsed ? val : _parseArrayValue(val, options);
late final int currentListLength;

if (chain.isNotEmpty && chain.last == '[]') {
final int? parentKey = int.tryParse(chain.slice(0, -1).join(''));

currentListLength = parentKey != null &&
val is List &&
val.firstWhereIndexedOrNull((int i, _) => i == parentKey) != null
? val.elementAt(parentKey).length

Check warning on line 145 in lib/src/extensions/decode.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/extensions/decode.dart#L143-L145

Added lines #L143 - L145 were not covered by tests
techouse marked this conversation as resolved.
Show resolved Hide resolved
: 0;
} else {
currentListLength = 0;
}

dynamic leaf = valuesParsed
? val
: _parseListValue(
val,
options,
currentListLength,
);

for (int i = chain.length - 1; i >= 0; --i) {
dynamic obj;
Expand Down
16 changes: 11 additions & 5 deletions lib/src/extensions/extensions.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import 'dart:math' show min;
import 'package:qs_dart/src/models/undefined.dart';

extension IterableExtension<T> on Iterable<T> {
/// Returns a new [Iterable] without [Undefined] elements.
Iterable<T> whereNotUndefined() => where((T el) => el is! Undefined);
/// Returns a new [Iterable] without elements of type [Q].
Iterable<T> whereNotType<Q>() => where((T el) => el is! Q);
}

extension ListExtension<T> on List<T> {
/// Returns a new [List] without [Undefined] elements.
List<T> whereNotUndefined() => where((T el) => el is! Undefined).toList();
/// Extracts a section of a list and returns a new list.
///
/// Modeled after JavaScript's `Array.prototype.slice()` method.
/// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
List<T> slice([int start = 0, int? end]) => sublist(
(start < 0 ? length + start : start).clamp(0, length),
(end == null ? length : (end < 0 ? length + end : end))
.clamp(0, length),
);
}

extension StringExtension on String {
Expand Down
4 changes: 4 additions & 0 deletions lib/src/models/decode_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ final class DecodeOptions with EquatableMixin {
this.parseLists = true,
this.strictDepth = false,
this.strictNullHandling = false,
this.throwOnLimitExceeded = false,
}) : allowDots = allowDots ?? decodeDotInKeys == true || false,
decodeDotInKeys = decodeDotInKeys ?? false,
_decoder = decoder,
Expand Down Expand Up @@ -110,6 +111,9 @@ final class DecodeOptions with EquatableMixin {
/// Set to true to decode values without `=` to `null`.
final bool strictNullHandling;

/// Set to `true` to throw an error when the limit is exceeded.
final bool throwOnLimitExceeded;

/// Set a [Decoder] to affect the decoding of the input.
final Decoder? _decoder;

Expand Down
1 change: 1 addition & 0 deletions lib/src/qs.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:convert' show latin1, utf8, Encoding;
import 'dart:typed_data' show ByteBuffer;

import 'package:collection/collection.dart' show IterableExtension;
techouse marked this conversation as resolved.
Show resolved Hide resolved
import 'package:qs_dart/src/enums/duplicates.dart';
import 'package:qs_dart/src/enums/format.dart';
import 'package:qs_dart/src/enums/list_format.dart';
Expand Down
29 changes: 17 additions & 12 deletions lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,14 @@
target_[target_.length] = source;
}

if (target is Set) {
target = target_.values.whereNotUndefined().toSet();
} else {
target = target_.values.whereNotUndefined().toList();
}
target = target_.values.any((el) => el is Undefined)
? SplayTreeMap.from({
for (final MapEntry<int, dynamic> entry in target_.entries)
if (entry.value is! Undefined) entry.key: entry.value,
})
: target is Set
? target_.values.toSet()
: target_.values.toList();
} else {
if (source is Iterable) {
// check if source is a list of maps and target is a list of maps
Expand All @@ -70,9 +73,11 @@
}
} else {
if (target is Set) {
target = Set.of(target)..addAll(source.whereNotUndefined());
target = Set.of(target)
..addAll(source.whereNotType<Undefined>());
} else {
target = List.of(target)..addAll(source.whereNotUndefined());
target = List.of(target)
..addAll(source.whereNotType<Undefined>());
}
}
} else if (source != null) {
Expand All @@ -96,7 +101,7 @@
}
} else if (source != null) {
if (target is! Iterable && source is Iterable) {
return [target, ...source.whereNotUndefined()];
return [target, ...source.whereNotType<Undefined>()];
}
return [target, source];
}
Expand All @@ -115,11 +120,11 @@

return [
if (target is Iterable)
...target.whereNotUndefined()
...target.whereNotType<Undefined>()

Check warning on line 123 in lib/src/utils.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/utils.dart#L123

Added line #L123 was not covered by tests
else if (target != null)
target,
if (source is Iterable)
...(source as Iterable).whereNotUndefined()
...(source as Iterable).whereNotType<Undefined>()

Check warning on line 127 in lib/src/utils.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/utils.dart#L127

Added line #L127 was not covered by tests
else
source,
];
Expand Down Expand Up @@ -381,9 +386,9 @@

if (obj is Iterable) {
if (obj is Set) {
item['obj'][item['prop']] = obj.whereNotUndefined().toSet();
item['obj'][item['prop']] = obj.whereNotType<Undefined>().toSet();

Check warning on line 389 in lib/src/utils.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/utils.dart#L389

Added line #L389 was not covered by tests
} else {
item['obj'][item['prop']] = obj.whereNotUndefined().toList();
item['obj'][item['prop']] = obj.whereNotType<Undefined>().toList();

Check warning on line 391 in lib/src/utils.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/utils.dart#L391

Added line #L391 was not covered by tests
techouse marked this conversation as resolved.
Show resolved Hide resolved
techouse marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand Down
90 changes: 88 additions & 2 deletions test/unit/decode_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ void main() {
expect(
QS.decode('a[1]=b&a=c', const DecodeOptions(listLimit: 20)),
equals({
'a': ['b', 'c']
'a': {1: 'b', 2: 'c'}
}),
);
expect(
Expand Down Expand Up @@ -818,7 +818,7 @@ void main() {
expect(
QS.decode('a[10]=1&a[2]=2', const DecodeOptions(listLimit: 20)),
equals({
'a': ['2', '1']
'a': {2: '2', 10: '1'}
}),
);
expect(
Expand Down Expand Up @@ -1768,4 +1768,90 @@ void main() {
},
);
});

group('parameter limit', () {
test('does not throw error when within parameter limit', () {
expect(
QS.decode('a=1&b=2&c=3',
const DecodeOptions(parameterLimit: 5, throwOnLimitExceeded: true)),
equals({'a': '1', 'b': '2', 'c': '3'}),
);
});

test('throws error when parameter limit exceeded', () {
expect(
() => QS.decode(
'a=1&b=2&c=3&d=4&e=5&f=6',
const DecodeOptions(parameterLimit: 3, throwOnLimitExceeded: true),
),
throwsA(isA<RangeError>()),
);
});

test('silently truncates when throwOnLimitExceeded is not given', () {
expect(
QS.decode(
'a=1&b=2&c=3&d=4&e=5',
const DecodeOptions(parameterLimit: 3),
),
equals({'a': '1', 'b': '2', 'c': '3'}),
);
});

test('silently truncates when parameter limit exceeded without error', () {
expect(
QS.decode(
'a=1&b=2&c=3&d=4&e=5',
const DecodeOptions(parameterLimit: 3, throwOnLimitExceeded: false),
),
equals({'a': '1', 'b': '2', 'c': '3'}),
);
});

test('allows unlimited parameters when parameterLimit set to Infinity', () {
expect(
QS.decode(
'a=1&b=2&c=3&d=4&e=5&f=6',
const DecodeOptions(parameterLimit: double.infinity),
),
equals({'a': '1', 'b': '2', 'c': '3', 'd': '4', 'e': '5', 'f': '6'}),
);
});
});

group('list limit tests', () {
test('does not throw error when list is within limit', () {
expect(
QS.decode(
'a[]=1&a[]=2&a[]=3',
const DecodeOptions(listLimit: 5, throwOnLimitExceeded: true),
),
equals({
'a': ['1', '2', '3']
}),
);
});

test('throws error when list limit exceeded', () {
expect(
() => QS.decode(
'a[]=1&a[]=2&a[]=3&a[]=4',
const DecodeOptions(listLimit: 3, throwOnLimitExceeded: true),
),
throwsA(isA<RangeError>()),
);
});

test('converts list to map if length is greater than limit', () {
expect(
QS.decode(
'a[1]=1&a[2]=2&a[3]=3&a[4]=4&a[5]=5&a[6]=6',
const DecodeOptions(listLimit: 5),
),
equals({
'a': {'1': '1', '2': '2', '3': '3', '4': '4', '5': '5', '6': '6'}
}),
);
});
});
techouse marked this conversation as resolved.
Show resolved Hide resolved
}
20 changes: 18 additions & 2 deletions test/unit/extensions/extensions_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ void main() {
group('IterableExtension', () {
test('whereNotUndefined', () {
const Iterable<dynamic> iterable = [1, 2, Undefined(), 4, 5];
final Iterable<dynamic> result = iterable.whereNotUndefined();
final Iterable<dynamic> result = iterable.whereNotType<Undefined>();
expect(result, isA<Iterable<dynamic>>());
expect(result, [1, 2, 4, 5]);
});
Expand All @@ -15,10 +15,26 @@ void main() {
group('ListExtension', () {
test('whereNotUndefined', () {
const List<dynamic> list = [1, 2, Undefined(), 4, 5];
final List<dynamic> result = list.whereNotUndefined();
final List<dynamic> result = list.whereNotType<Undefined>().toList();
expect(result, isA<List<dynamic>>());
expect(result, [1, 2, 4, 5]);
});

test('slice', () {
const List<String> animals = [
'ant',
'bison',
'camel',
'duck',
'elephant',
];
expect(animals.slice(2), ['camel', 'duck', 'elephant']);
expect(animals.slice(2, 4), ['camel', 'duck']);
expect(animals.slice(1, 5), ['bison', 'camel', 'duck', 'elephant']);
expect(animals.slice(-2), ['duck', 'elephant']);
expect(animals.slice(2, -1), ['camel', 'duck']);
expect(animals.slice(), ['ant', 'bison', 'camel', 'duck', 'elephant']);
});
});

group('StringExtensions', () {
Expand Down
4 changes: 2 additions & 2 deletions test/unit/uri_extension_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ void main() {
Uri.parse('$testUrl?a[1]=b&a=c')
.queryParametersQs(const DecodeOptions(listLimit: 20)),
equals({
'a': ['b', 'c']
'a': {1: 'b', 2: 'c'}
}),
);
expect(
Expand Down Expand Up @@ -864,7 +864,7 @@ void main() {
Uri.parse('$testUrl?a[10]=1&a[2]=2')
.queryParametersQs(const DecodeOptions(listLimit: 20)),
equals({
'a': ['2', '1']
'a': {2: '2', 10: '1'}
}),
);
expect(
Expand Down
Loading